"""Tab widget displaying pipeline results."""

from __future__ import annotations

import math
from typing import List, Optional, Tuple

import numpy as np

from PyQt5.QtCore import Qt, QTimer, QPoint, QEvent, pyqtSignal
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import QLabel, QScrollArea, QSizePolicy, QTabWidget

from ..core.utils import to_display

class DraggableScrollArea(QScrollArea):
    positionChanged = pyqtSignal(float, float)

    def __init__(self) -> None:
        super().__init__()
        self.setWidgetResizable(False)
        self.setAlignment(Qt.AlignCenter)
        self._dragging = False
        self._last_pos = QPoint()
        self._zoom_factor = 1.0
        self._suppress_emit = False
        self.horizontalScrollBar().valueChanged.connect(lambda _: self._emit_position())
        self.verticalScrollBar().valueChanged.connect(lambda _: self._emit_position())

    def set_zoom_factor(self, zoom: float) -> None:
        self._zoom_factor = zoom
        self._update_cursor()

    def _can_drag(self) -> bool:
        hbar = self.horizontalScrollBar()
        vbar = self.verticalScrollBar()
        return self._zoom_factor > 1.0 or hbar.maximum() > 0 or vbar.maximum() > 0

    def _update_cursor(self) -> None:
        if self._dragging:
            self.viewport().setCursor(Qt.ClosedHandCursor)
        elif self._can_drag():
            self.viewport().setCursor(Qt.OpenHandCursor)
        else:
            self.viewport().setCursor(Qt.ArrowCursor)

    def eventFilter(self, source, event):  # type: ignore[override]
        if source is self.widget() or source is self.viewport():
            if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton and self._can_drag():
                self._dragging = True
                self._last_pos = event.globalPos()
                self._update_cursor()
                event.accept()
                return True
            if event.type() == QEvent.MouseMove and self._dragging:
                current_pos = event.globalPos()
                delta = current_pos - self._last_pos
                self._last_pos = current_pos
                hbar = self.horizontalScrollBar()
                vbar = self.verticalScrollBar()
                hbar.setValue(hbar.value() - delta.x())
                vbar.setValue(vbar.value() - delta.y())
                event.accept()
                return True
            if event.type() in {QEvent.MouseButtonRelease, QEvent.MouseButtonDblClick} and self._dragging:
                if event.button() == Qt.LeftButton:
                    self._dragging = False
                    self._update_cursor()
                    event.accept()
                    return True
            if event.type() == QEvent.Wheel:
                self.wheelEvent(event)
                return True
        return super().eventFilter(source, event)

    def _emit_position(self) -> None:
        if self._suppress_emit:
            return
        anchor = self.get_anchor()
        self.positionChanged.emit(*anchor)

    def get_anchor(self) -> Tuple[float, float]:
        content = self.widget()
        if content is None or content.width() <= 0 or content.height() <= 0:
            return (0.5, 0.5)
        viewport_size = self.viewport().size()
        hbar = self.horizontalScrollBar()
        vbar = self.verticalScrollBar()
        center_x = hbar.value() + viewport_size.width() / 2.0
        center_y = vbar.value() + viewport_size.height() / 2.0
        ratio_x = 0.5 if content.width() <= 0 else center_x / float(content.width())
        ratio_y = 0.5 if content.height() <= 0 else center_y / float(content.height())
        return (max(0.0, min(1.0, ratio_x)), max(0.0, min(1.0, ratio_y)))

    def apply_anchor(self, ratio_x: float, ratio_y: float) -> None:
        content = self.widget()
        if content is None or content.width() <= 0 or content.height() <= 0:
            return
        viewport_size = self.viewport().size()
        hbar = self.horizontalScrollBar()
        vbar = self.verticalScrollBar()

        target_center_x = ratio_x * float(content.width())
        target_center_y = ratio_y * float(content.height())

        target_value_x = int(round(target_center_x - viewport_size.width() / 2.0))
        target_value_y = int(round(target_center_y - viewport_size.height() / 2.0))

        max_x = hbar.maximum()
        max_y = vbar.maximum()
        clamped_x = max(0, min(target_value_x, max_x)) if max_x > 0 else 0
        clamped_y = max(0, min(target_value_y, max_y)) if max_y > 0 else 0

        self._suppress_emit = True
        hbar.setValue(clamped_x)
        vbar.setValue(clamped_y)
        self._suppress_emit = False
        self._emit_position()

    def wheelEvent(self, event) -> None:  # type: ignore[override]
        delta = event.angleDelta().y()
        parent = self.parentWidget()
        while parent is not None and not hasattr(parent, "handle_wheel_zoom"):
            parent = parent.parentWidget()
        if delta == 0 or parent is None:
            super().wheelEvent(event)
            return
        modifiers = event.modifiers()
        ctrl = modifiers & Qt.ControlModifier
        handled = parent.handle_wheel_zoom(self, event, ctrl_active=bool(ctrl))
        if handled:
            return
        if ctrl:
            event.accept()
            return
        super().wheelEvent(event)

class ImageTabWidget(QTabWidget):
    tabActivated = pyqtSignal(int)

    def __init__(self) -> None:
        super().__init__()
        self.setTabsClosable(False)
        self.zoom_factor = 1.0
        self._min_zoom = 0.1
        self._max_zoom = 6.0
        self._zoom_step = 1.25
        self._scroll_anchor: Tuple[float, float] = (0.5, 0.5)
        self.currentChanged.connect(self._on_tab_changed)

    def update_images(self, items: List[Tuple[str, np.ndarray]], active_index: int = 0) -> None:
        self.clear()
        for title, image in items:
            label = QLabel()
            label.setAlignment(Qt.AlignCenter)
            label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
            pixmap = self._to_pixmap(image)
            label.setProperty("original_pixmap", pixmap)
            scroll = DraggableScrollArea()
            scroll.setWidget(label)
            scroll.set_zoom_factor(self.zoom_factor)
            label.installEventFilter(scroll)
            scroll.viewport().installEventFilter(scroll)
            scroll.positionChanged.connect(self._on_scroll_position_changed)
            self.addTab(scroll, title)
        self._refresh_scaling()
        if items:
            clamped_index = max(0, min(active_index, self.count() - 1))
            self.setCurrentIndex(clamped_index)
            self._on_tab_changed(self.currentIndex())

    def resizeEvent(self, event) -> None:  # type: ignore[override]
        super().resizeEvent(event)
        QTimer.singleShot(0, self._refresh_scaling)

    @staticmethod
    def _to_pixmap(image: np.ndarray) -> QPixmap:
        display = to_display(image)
        height, width, channels = display.shape
        bytes_per_line = channels * width
        q_image = QImage(display.data, width, height, bytes_per_line, QImage.Format_BGR888)
        return QPixmap.fromImage(q_image)

    def _refresh_scaling(self) -> None:
        for idx in range(self.count()):
            widget = self.widget(idx)
            if isinstance(widget, DraggableScrollArea):
                widget.set_zoom_factor(self.zoom_factor)
                label = widget.widget()
                if isinstance(label, QLabel):
                    self._apply_scaled_pixmap(label)
                widget.apply_anchor(*self._scroll_anchor)

    def save_view_state(self) -> Optional[Tuple[int, float, float, float]]:
        if self.count() == 0:
            return None
        index = self.currentIndex()
        if index < 0:
            index = 0
        anchor_x, anchor_y = self._scroll_anchor
        return (index, self.zoom_factor, anchor_x, anchor_y)

    def restore_view_state(
        self,
        state: Optional[Tuple[int, float, float, float]],
        target_index: Optional[int] = None,
    ) -> None:
        if self.count() == 0:
            return
        if state is None:
            if target_index is not None and 0 <= target_index < self.count():
                self.setCurrentIndex(target_index)
            return
        index, zoom, anchor_x, anchor_y = state
        if target_index is not None:
            index = target_index
        index = max(0, min(index, self.count() - 1))
        self._scroll_anchor = (anchor_x, anchor_y)
        self.set_zoom(zoom)
        self.setCurrentIndex(index)
        widget = self.widget(index)
        if isinstance(widget, DraggableScrollArea):
            widget.apply_anchor(anchor_x, anchor_y)

    def _apply_scaled_pixmap(self, label: QLabel) -> None:
        pixmap = label.property("original_pixmap")
        if not isinstance(pixmap, QPixmap) or pixmap.isNull():
            return
        target_width = max(1, int(pixmap.width() * self.zoom_factor))
        target_height = max(1, int(pixmap.height() * self.zoom_factor))
        scaled = pixmap.scaled(target_width, target_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        label.setPixmap(scaled)
        label.resize(scaled.size())
        parent = label.parent()
        if isinstance(parent, DraggableScrollArea):
            parent._update_cursor()

    def set_zoom(self, factor: float) -> None:
        clamped = max(self._min_zoom, min(self._max_zoom, factor))
        if math.isclose(clamped, self.zoom_factor, rel_tol=1e-3):
            return
        self.zoom_factor = clamped
        self._refresh_scaling()

    def zoom_in(self) -> None:
        self.set_zoom(self.zoom_factor * self._zoom_step)

    def zoom_out(self) -> None:
        self.set_zoom(self.zoom_factor / self._zoom_step)

    def reset_zoom(self) -> None:
        self.set_zoom(1.0)

    def _on_scroll_position_changed(self, ratio_x: float, ratio_y: float) -> None:
        self._scroll_anchor = (ratio_x, ratio_y)

    def handle_wheel_zoom(self, scroll: DraggableScrollArea, event, ctrl_active: bool = False) -> bool:
        delta = event.angleDelta().y()
        if delta == 0:
            delta = event.pixelDelta().y()
        if delta == 0 or not ctrl_active:
            return False
        label = scroll.widget()
        if not isinstance(label, QLabel):
            return False
        width = label.width()
        height = label.height()
        if width <= 0 or height <= 0:
            return False
        hbar = scroll.horizontalScrollBar()
        vbar = scroll.verticalScrollBar()
        viewport = scroll.viewport()
        global_pos = event.globalPos()
        pos = viewport.mapFromGlobal(global_pos)
        content_x = hbar.value() + pos.x()
        content_y = vbar.value() + pos.y()
        ratio_x = max(0.0, min(1.0, content_x / float(width)))
        ratio_y = max(0.0, min(1.0, content_y / float(height)))
        self._scroll_anchor = (ratio_x, ratio_y)

        if delta > 0:
            new_zoom = self.zoom_factor * self._zoom_step
        else:
            new_zoom = self.zoom_factor / self._zoom_step

        prev_zoom = self.zoom_factor
        self.set_zoom(new_zoom)
        if math.isclose(prev_zoom, self.zoom_factor, rel_tol=1e-3):
            return False
        scroll.apply_anchor(ratio_x, ratio_y)
        event.accept()
        return True

    def _on_tab_changed(self, index: int) -> None:
        widget = self.widget(index)
        if isinstance(widget, DraggableScrollArea):
            widget.apply_anchor(*self._scroll_anchor)
        self.tabActivated.emit(index)

